/**
 *  Serponix is an arcade game in focus to multiplayer based on the classic game Snake.
 *  Copyright (C) 2010 - 2011  Daniel Vala
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License,
 *  or  any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.

 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *  If you have any question do not hesitate to contact author
 *  on e-mail address: danielvala42@gmail.com
 */
package com.serponix.net;

import com.serponix.gui.lobby.ModelLobbyClient;
import com.serponix.settings.SettingsFactory;
import com.serponix.game.*;
import java.awt.Color;
import java.awt.Dimension;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

/**
 * Obsluhuje výměnu dat se serverem
 * @author Daniel Vala
 */
public class UDPClient implements Client {

    //  private LobbyProClienta lobbyProClienta;
    private ModelLobbyClient modelLobbyClient;
    private DatagramSocket socket;
    private ModelClient model;
    private InetAddress serverIP;
    private boolean prijemLobby;
    private boolean cancel;
    private final int PORT;

    public UDPClient() {
        PORT = SettingsFactory.getNastaveniHry().getPort();
        prijemLobby = true;
        try {
            socket = new DatagramSocket();
        } catch (SocketException ex) {
            System.exit(1);
        }
    }

    @Override
    public void setLobby(ModelLobbyClient modelLobbyClient) {
        this.modelLobbyClient = modelLobbyClient;
    }

    @Override
    public void setModel(ModelClient model) {
        this.model = model;
    }

    @Override
    public InetAddress getServerIP() {
        return serverIP;
    }

    @Override
    public void odpojSe() {
        poslatData(Informace.KONEC + Informace.ODDELOVAC1);
        cancel = true;
        socket.close();
    }

    @Override
    public void run() {
        prijemStavuLobby();
        prijemDat();
    }

    /**
     * přijímá stav lobby od serveru
     */
    @Override
    public void prijemStavuLobby() {
        byte[] data = new byte[20000];
        while (prijemLobby) {

            DatagramPacket packet = new DatagramPacket(data, data.length);
            try {
                socket.receive(packet);
            } catch (SocketException ex) {
                if (!cancel) {
                    JOptionPane.showMessageDialog(null, "Neočekávané uzavření socketu.", "Chyba spojení", JOptionPane.ERROR_MESSAGE);
                }
                return;
            } catch (IOException ex) {
                JOptionPane.showMessageDialog(null, "Chyba při čtení ze sítě.", "Chyba spojení", JOptionPane.ERROR_MESSAGE);
            }

            String prijataZprava;
            try {
                prijataZprava = new String(packet.getData(), 0, packet.getLength(), "UTF-8");
            } catch (UnsupportedEncodingException ex) {
                throw new RuntimeException(ex);
            }
            StringTokenizer st = new StringTokenizer(prijataZprava, Informace.ODDELOVAC1);
            int druhZpravy = 0;
            try {
                druhZpravy = Integer.parseInt(st.nextToken());
            } catch (NumberFormatException ex) {
                System.err.println("Příjem nerozeznatelného pokynu od serveru:");
                System.err.println(prijataZprava);
            }

            switch (druhZpravy) {
                case Informace.START:
                    int pocetHracu = 0;
                    Dimension velikostHerniPlochy = null;
                    Mapa mapa = new Mapa();
                    String podZprava = st.nextToken();
                    st = new StringTokenizer(podZprava, Informace.ODDELOVAC2);
                    while (st.hasMoreTokens()) {
                        String atributNastaveni = st.nextToken();
                        StringTokenizer st2 = new StringTokenizer(atributNastaveni, Informace.ODDELOVAC3);
                        int typ = Integer.parseInt(st2.nextToken());
                        switch (typ) {
                            case Informace.POCET_HRACU:
                                pocetHracu = Integer.parseInt(st2.nextToken());
                                break;
                            case Informace.VELIKOST_HERNI_PLOCHY:
                                velikostHerniPlochy = new Dimension(Integer.parseInt(st2.nextToken()), Integer.parseInt(st2.nextToken()));
                                break;
                            case Informace.MAPA:
                                mapa = new Mapa(st2.nextToken());
                                break;
                            default:
                                st2.nextToken();
                        }
                    }
                    modelLobbyClient.startHry(pocetHracu, velikostHerniPlochy, mapa);
                    prijemLobby = false;
                    break;
                case Informace.ZPRAVA:
                    String informace = st.nextToken();
                    zobrazInformaci(informace);
                    break;
                case Informace.HRAC:
                    String podZprava2 = st.nextToken();
                    st = new StringTokenizer(podZprava2, Informace.ODDELOVAC3);
                    int cisloHrace = Integer.parseInt(st.nextToken());
                    String jmeno = st.nextToken();
                    modelLobbyClient.setHrac(cisloHrace, jmeno);
                    break;
                case Informace.NAZEV_MAPY:
                    modelLobbyClient.setStitekMapy(st.nextToken());
                    break;
                case Informace.MOD_HRY:
                    modelLobbyClient.setModHry(st.nextToken());
                    break;
                case Informace.RYCHLOST:
                    modelLobbyClient.setRychlostHry(st.nextToken());
                    break;
                case Informace.KONEC:
                    JOptionPane.showMessageDialog(null, "Server ukončil hru.", "Zpráva", JOptionPane.INFORMATION_MESSAGE);
                    modelLobbyClient.konec();
                    break;
                case Informace.VYHODIT:
                    JOptionPane.showMessageDialog(null, "Byl jste vyhozen ze hry.", "Zpráva", JOptionPane.INFORMATION_MESSAGE);
                    modelLobbyClient.konec();
                    break;
                default:
                    System.err.println("Příjem nerozeznatelného pokynu od serveru:");
                    System.err.println(prijataZprava);
            }
        }
    }

    /**
     * přijímá stav hry, články hadů a jejich střel, všechny štítky a zprávy
     */
    @Override
    public void prijemDat() {
        while (true) {
            byte[] data = new byte[20000];
            DatagramPacket packet = new DatagramPacket(data, data.length);
            try {
                socket.receive(packet);
            } catch (SocketException ex) {
                if (!cancel) {
                    JOptionPane.showMessageDialog(null, "Neočekávané uzavření socketu.", "Chyba spojení", JOptionPane.ERROR_MESSAGE);
                    model.ukonciHru();
                }
                return;
            } catch (IOException e) {
                zobrazInformaci(e.toString());
            }
            String prijataZprava;
            try {
                prijataZprava = new String(packet.getData(), 0, packet.getLength(), "UTF-8");
            } catch (UnsupportedEncodingException ex) {
                throw new RuntimeException(ex);
            }
            StringTokenizer st = new StringTokenizer(prijataZprava, Informace.ODDELOVAC1);
            int druhZpravy = 0;
            try {
                druhZpravy = Integer.parseInt(st.nextToken());
            } catch (NumberFormatException ex) {
                System.err.println("Příjem nerozeznatelného pokynu od serveru.");
            }

            switch (druhZpravy) {
                case Informace.POLICKO:
                    //    model.smazVsechnyPolicka();
                    String podZprava = st.nextToken();
                    st = new StringTokenizer(podZprava, Informace.ODDELOVAC2);
                    // přečtení a přidání všech políček
                    while (st.hasMoreTokens()) {
                        String policko = st.nextToken();
                        StringTokenizer st2 = new StringTokenizer(policko, Informace.ODDELOVAC3);

                        int typ = Integer.parseInt(st2.nextToken());
                        int x = Integer.parseInt(st2.nextToken());
                        int y = Integer.parseInt(st2.nextToken());
                        Color barva = null;
                        int informace = 0;
                        Direction smer = null;
                        boolean neviditelny = false;
                        if (st2.hasMoreTokens()) {
                            barva = Color.decode(st2.nextToken());
                        }
                        if (st2.hasMoreTokens()) {
                            informace = Integer.parseInt(st2.nextToken());
                        }
                        if (st2.hasMoreTokens()) {
                            int a = Integer.parseInt(st2.nextToken());
                            smer = Direction.prevedCisloNaSmer(a);
                        }
                        if (st2.hasMoreTokens()) {
                            if (Integer.parseInt(st2.nextToken()) == Informace.NEVIDITELNOST) {
                                neviditelny = true;
                            }
                        }
                        Policko p = new Policko(typ, x, y, barva, informace, smer, neviditelny);

                        model.pridejPolicko(p);
                    }
                    break;
                case Informace.SKORE:
                    String skore = st.nextToken();
                    st = new StringTokenizer(skore, Informace.ODDELOVAC3);
                    int i = 0;
                    while (st.hasMoreTokens()) {
                        String text = st.nextToken();
                        model.setAllSkoreText(i, text);
                        i++;
                    }
                    break;
                case Informace.INFO:
                    String informace = st.nextToken();
                    model.getPanelZprav().addMessage(informace);
                    break;
                case Informace.KROK:
                    model.krok();
                    break;
                case Informace.VITEZSTVI:
                    JOptionPane.showMessageDialog(null, st.nextToken(), "KONEC HRY", JOptionPane.INFORMATION_MESSAGE);
                    model.ukonciHru();
                    break;
                case Informace.KONEC:
                    JOptionPane.showMessageDialog(null, "Server ukončil hru.", "KONEC HRY", JOptionPane.INFORMATION_MESSAGE);
                    model.ukonciHru();
                    break;
                default:
                    System.err.println("Příjem nerozeznatelného pokynu od serveru:");
                    System.err.println(prijataZprava);
            }
        }
    }

    /**
     * vyhledá hry na některých IP adresách
     * @return seznam IP adres, na kterých jsou založeny hry
     * @throws java.io.IOException
     */
    @Override
    public List<InetAddress> vyhledatHry() throws IOException {
        String message = Informace.HLEDANI + Informace.ODDELOVAC1;

        int a = 192;
        int b = 168;
        int c = 1;
        int d = 0;
        byte[] data = message.getBytes();

        while (d != 255) {
            socket.send(new DatagramPacket(data, data.length, InetAddress.getByName(a + "." + b + "." + c + "." + d), PORT));
            d++;
        }


        socket.send(new DatagramPacket(data, data.length, InetAddress.getByName("localhost"), PORT));

        DatagramPacket packet = new DatagramPacket(data, data.length);
        List<InetAddress> nalezeneAdresy = new ArrayList<InetAddress>();
        for (int i = 0; i < 100; i++) {
            try {
                socket.setSoTimeout(1000);
                socket.receive(packet);
            } catch (SocketTimeoutException ex) {
                return nalezeneAdresy;
            }
            String prijataZprava = new String(packet.getData(), 0, packet.getLength(), "UTF-8");
            if (prijataZprava.startsWith(Informace.PRIJEM + Informace.ODDELOVAC1)) {
                nalezeneAdresy.add(packet.getAddress());
            }
        }
        return nalezeneAdresy;
    }

    /**
     * Připojí se k serveru podle zadané IP adresy
     * @param jmenoHrace jméno hráče
     * @param serverIP IP adresa serveru
     * @throws java.net.SocketTimeoutException
     * @throws java.io.IOException
     */
    @Override
    public boolean pripojitSeKServeru(InetAddress serverIP) throws SocketTimeoutException, IOException {
        String message = Informace.NOVY_HRAC + Informace.ODDELOVAC1 + SettingsFactory.getNastaveniHry().getJmeno(0);
        byte[] data = message.getBytes();
        socket.send(new DatagramPacket(data, data.length, serverIP, PORT));
        byte[] data2 = new byte[2000];
        DatagramPacket packet = new DatagramPacket(data2, data2.length);
        socket.setSoTimeout(1000); // pokud server neodpoví za 1 vteřinu, vyhodí výjimku
        socket.receive(packet);
        String prijataZprava = new String(packet.getData(), 0, packet.getLength(), "UTF-8");
        boolean cislo = true;
        int info = -1;
        try {
            info = Integer.parseInt(prijataZprava);
        } catch (NumberFormatException ex) {
            cislo = false;
        }
        if (cislo) {
            if (info == Informace.PLNY_SERVER) {
                return false;
            }
        }
        socket.setSoTimeout(0); // nastavení timeoutu na nekonečno
        this.serverIP = serverIP;
        return true;
    }

    /**
     * pošle data serveru
     * @param dataKPoslani
     */
    @Override
    public void poslatData(String dataKPoslani) {
        byte[] data;
        try {
            data = dataKPoslani.getBytes("UTF-8");
        } catch (UnsupportedEncodingException ex) {
            throw new RuntimeException(ex);
        }
        try {
            socket.send(new DatagramPacket(data, data.length, serverIP, PORT));
        } catch (IOException ex) {
            System.err.println("chyba poslání směru");
        }
    }

    /**
     * zobrazí informaci buď v lobby, nebo ve hře
     * @param messageToDisplay
     */
    private void zobrazInformaci(final String messageToDisplay) {
        SwingUtilities.invokeLater(
                new Runnable() {

            @Override
                    public void run() {
                        if (model != null) {
                            model.getPanelZprav().addMessage(String.format(messageToDisplay));
                        } else {
                            modelLobbyClient.zobrazZpravu(messageToDisplay);
                        }
                    }
                });
    }
}
